<?php
/***************************************************************
*  Copyright notice
*  (c) 2009, jianyuzhu@gmail.com
*  All rights reserved
*  This script is part of the PPEMI project.
***************************************************************/

/*
Minimal Example:

	$http = new CoreHttpClient();
	$http->Connect("somehost", 80) or die("Connect problem");
	$status = $http->Get("/index.html");
	if( $status != 200) {
		die("Problem : " . $http->getStatusMessage());
	} else {
		echo $http->getBody();
	}
	$http->Disconnect();

Persistent Example:

	$http = new CoreHttpClient("dir.yahoo.com", 80);
	$http->addHeader("Host", "dir.yahoo.com");
	$http->addHeader("Connection", "keep-alive");
	
	if( $http->Get("/Reference/Libraries/") == 200) $page1 = $http->getBody();
	if( $http->Get("/News_and_Media/") == 200 ) $page2 = $http->getBody();
	$http->disconnect();
*/

class CoreHttpClient {
	var $url; 			// array containg server URL, similar to parseurl() returned array
	var $reply; 		// response code
	var $replyString; 	// full response
	var $protocolVersion = '1.1';
	var $requestHeaders, $requestBody;
	var $socket = false;
	// proxy stuff
	var $useProxy = false;
	var $proxyHost, $proxyPort;

	/**
	 * httpClient constructor
	 * Note: when host and port are defined, the connection is immediate
	 * @seeAlso connect
	 **/
	function CoreHttpClient($host = '', $port = '') {
		if( util_not_null($host)) {
			$this->connect($host, $port);
		}
	}

	/**
	 * turn on proxy support
	 * @param proxyHost proxy host address eg "proxy.mycorp.com"
	 * @param proxyPort proxy port usually 80 or 8080
	 **/
	function setProxy($proxyHost, $proxyPort) {
		$this->useProxy = true;
		$this->proxyHost = $proxyHost;
		$this->proxyPort = $proxyPort;
	}

	/**
	 * setProtocolVersion
	 * define the HTTP protocol version to use
	 * @param version string the version number with one decimal: "0.9", "1.0", "1.1"
	 * when using 1.1, you MUST set the mandatory headers "Host"
	 * @return boolean false if the version number is bad, true if ok
	 **/
	function setProtocolVersion($version) {
		if( ($version > 0) && ($version <= 1.1) ) {
			$this->protocolVersion = $version;
			return true;
		} else {
			return false;
		}
	}

	/**
	 * set a username and password to access a protected resource
	 * Only "Basic" authentication scheme is supported yet
	 * @param username string - identifier
	 * @param password string - clear password
	 **/
	function setCredentials($username, $password) {
		$this->addHeader('Authorization', 'Basic ' . base64_encode($username . ':' . $password));
	}
	
	/**
	 * define a set of HTTP headers to be sent to the server
	 * header names are lowercased to avoid duplicated headers
	 * @param headers hash array containing the headers as headerName => headerValue pairs
	 **/
	function setHeaders($headers) {
		if( is_array($headers)) {
			reset($headers);
			while(list($name, $value) = each($headers)) {
				$this->requestHeaders[$name] = $value;
			}
		}
	}
		
	/**
	 * addHeader
	 * set a unique request header
	 * @param headerName the header name
	 * @param headerValue the header value, ( unencoded)
	 **/
	function addHeader($headerName, $headerValue) {
		$this->requestHeaders[$headerName] = $headerValue;
	}
	
	/**
	 * removeHeader
	 * unset a request header
	 * @param headerName the header name
	 **/
	function removeHeader($headerName) {
		unset($this->requestHeaders[$headerName]);
	}

	/**
	 * Connect
	 * open the connection to the server
	 * @param host string server address (or IP)
	 * @param port string server listening port - defaults to 80
	 * @return boolean false is connection failed, true otherwise
	 **/
	function Connect($host, $port = '') {
		$this->url['scheme'] = 'http';
		$this->url['host'] = $host;
		if( util_not_null($port)) $this->url['port'] = $port;
		
		return true;
	}
	
	/**
	 * Disconnect
	 * close the connection to theserver
	 **/
	function Disconnect() {
		if( $this->socket) fclose($this->socket);
	}

	/**
	 * head
	 * issue a HEAD request
	 * @param uri string URI of the document
	 * @return string response status code (200 if ok)
	 * @seeAlso getHeaders()
	 **/
	function Head($uri) {
		$this->responseHeaders = $this->responseBody = '';
		
		$uri = $this->makeUri($uri);
		
		if( $this->sendCommand('HEAD ' . $uri . ' HTTP/' . $this->protocolVersion)) {
			$this->processReply();
		}
		
		return $this->reply;
	}
	
	/**
	 * get
	 * issue a GET http request
	 * @param uri URI (path on server) or full URL of the document
	 * @return string response status code (200 if ok)
	 * @seeAlso getHeaders(), getBody()
	 **/
	function Get($url) {
		$this->responseHeaders = $this->responseBody = '';
		
		$uri = $this->makeUri($url);
		
		if( $this->sendCommand('GET ' . $uri . ' HTTP/' . $this->protocolVersion)) {
			$this->processReply();
		}
		
		return $this->reply;
	}
		
	/**
	 * Post
	 * issue a POST http request
	 * @param uri string URI of the document
	 * @param query_params array parameters to send in the form "parameter name" => value
	 * @return string response status code (200 if ok)
	 * @example 
	 * $params = array( "login" => "tiger", "password" => "secret" );
	 * $http->post( "/login.php", $params );
	 **/
	function Post($uri, $query_params = '') {
		$uri = $this->makeUri($uri);
		
		if( is_array($query_params)) {
			$postArray = array();
			reset($query_params);
			while(list($k, $v) = each($query_params)) {
				$postArray[] = urlencode($k) . '=' . urlencode($v);
			}
			
			$this->requestBody = implode('&', $postArray);
		}
		// set the content type for post parameters
		$this->addHeader('Content-Type', 'application/x-www-form-urlencoded');
		
		if( $this->sendCommand('POST ' . $uri . ' HTTP/' . $this->protocolVersion)) {
			$this->processReply();
		}
		
		$this->removeHeader('Content-Type');
		$this->removeHeader('Content-Length');
		$this->requestBody = '';
		
		return $this->reply;
	}
	
	/**
	 * Put
	 * Send a PUT request
	 * PUT is the method to sending a file on the server. it is *not* widely supported
	 * @param uri the location of the file on the server. dont forget the heading "/"
	 * @param filecontent the content of the file. binary content accepted
	 * @return string response status code 201 (Created) if ok
	 * @see RFC2518 "HTTP Extensions for Distributed Authoring WEBDAV"
	 **/
	function Put($uri, $filecontent) {
		$uri = $this->makeUri($uri);
		$this->requestBody = $filecontent;
		
		if( $this->sendCommand('PUT ' . $uri . ' HTTP/' . $this->protocolVersion)) {
			$this->processReply();
		}
		
		return $this->reply;
	}

	/**
	 * getHeaders
	 * return the response headers
	 * to be called after a Get() or Head() call
	 * @return array headers received from server in the form headername => value
	 * @seeAlso get, head
	 **/
	function getHeaders() {
		return $this->responseHeaders;
	}

	/**
	 * getHeader
	 * return the response header "headername"
	 * @param headername the name of the header
	 * @return header value or NULL if no such header is defined
	 **/
	function getHeader($headername) {
		return $this->responseHeaders[$headername];
	}

	/**
	 * getBody
	 * return the response body
	 * invoke it after a Get() call for instance, to retrieve the response
	 * @return string body content
	 * @seeAlso get, head
	 **/
	function getBody() {
		return $this->responseBody;
	}
	
	/**
	 * getStatus return the server response's status code
	 * @return string a status code
	 * code are divided in classes (where x is a digit)
	 *- 20x : request processed OK
	 *- 30x : document moved
	 *- 40x : client error ( bad url, document not found, etc...)
	 *- 50x : server error 
	 * @see RFC2616 "Hypertext Transfer Protocol -- HTTP/1.1"
	 **/
	function getStatus() {
		return $this->reply;
	}
	
	/** 
	 * getStatusMessage return the full response status, of the form "CODE Message"
	 * eg. "404 Document not found"
	 * @return string the message 
	 **/
	function getStatusMessage() {
		return $this->replyString;
	}

	/**
	 * @scope only protected or private methods below
	 **/
	
	/** 
	 * send a request
	 * data sent are in order
	 * a) the command
	 * b) the request headers if they are defined
	 * c) the request body if defined
	 * @return string the server repsonse status code
	 **/
	function sendCommand($command) {
		$this->responseHeaders = array();
		$this->responseBody = '';
		
		// connect if necessary
		if( ($this->socket == false) || (feof($this->socket)) ) {
			if( $this->useProxy) {
				$host = $this->proxyHost;
				$port = $this->proxyPort;
			} else {
				$host = $this->url['host'];
				$port = $this->url['port'];
			}
		
			if( !util_not_null($port)) $port = 80;
			
			if( !$this->socket = fsockopen($host, $port, $this->reply, $this->replyString)) {
				return false;
			}
		
			if( util_not_null($this->requestBody)) {

				$this->addHeader('Content-Length', strlen($this->requestBody));
			}
			
			$this->request = $command;
			$cmd = $command . "\r\n";
			if( is_array($this->requestHeaders)) {
				reset($this->requestHeaders);
				while(list($k, $v) = each($this->requestHeaders)) {
					$cmd .= $k . ': ' . $v . "\r\n";
				}
			}
			
			if( util_not_null($this->requestBody)) {
				$cmd .= "\r\n" . $this->requestBody;
			}
			
			// unset body (in case of successive requests)
			$this->requestBody = '';
			
			fputs($this->socket, $cmd . "\r\n");
			
			return true;
		}
	}
	
	function processReply() {
		$this->replyString = trim(fgets($this->socket, 1024));
		
		if( preg_match('|^HTTP/\S+ (\d+) |i', $this->replyString, $a )) {
			$this->reply = $a[1];
		} else {
			$this->reply = 'Bad Response';
		}
		//get response headers and body
		$this->responseHeaders = $this->processHeader();
		$this->responseBody = $this->processBody();
		
		return $this->reply;
	}

	/**
	 * processHeader() reads header lines from socket until the line equals $lastLine
	 * @scope protected
	 * @return array of headers with header names as keys and header content as values
	 **/
	function processHeader($lastLine = "\r\n") {
		$headers = array();
		$finished = false;
		
		while ( (!$finished) && (!feof($this->socket)) ) {
			$str = fgets($this->socket, 1024);
			$finished = ($str == $lastLine);
			if( !$finished) {
				list($hdr, $value) = split(': ', $str, 2);
				// nasty workaround broken multiple same headers (eg. Set-Cookie headers) @FIXME 
				if( isset($headers[$hdr]) ) {
					$headers[$hdr] .= '; ' . trim($value);
				} else {
					$headers[$hdr] = trim($value);
				}
			}
		}
		
		return $headers;
	}
	
	/**
	 * processBody() reads the body from the socket
	 * the body is the "real" content of the reply
	 * @return string body content 
	 * @scope private
	 **/
	function processBody() {
		$data = '';
		$counter = 0;
		
		do {
			$status = socket_get_status($this->socket);
			if( $status['eof'] == 1) {
				break;
			}
			
			if( $status['unread_bytes'] > 0 ) {
				$buffer = fread($this->socket, $status['unread_bytes']);
				$counter = 0;
			} else {
				$buffer = fread($this->socket, 128);
				$counter++;
				usleep(2);
			}
			
			$data .= $buffer;
		} while ( ($status['unread_bytes'] > 0) || ($counter++ < 10) );
		
		return $data;
	}

	/**
	 * Calculate and return the URI to be sent ( proxy purpose )
	 * @param the local URI
	 * @return URI to be used in the HTTP request
	 * @scope private
	 **/
	function makeUri($uri) {
		$a = parse_url($uri);
		
		if( (isset($a['scheme'])) && (isset($a['host'])) ) {
			$this->url = $a;
		} else {
			unset($this->url['query']);
			unset($this->url['fragment']);
			$this->url = array_merge($this->url, $a);
		}
		
		if( $this->useProxy) {
			$requesturi = 'http://' . $this->url['host'] . (empty($this->url['port']) ? '' : ':' . $this->url['port']) . $this->url['path'] . (empty($this->url['query']) ? '' : '?' . $this->url['query']);
		} else {
			$requesturi = $this->url['path'] . (empty($this->url['query']) ? '' : '?' . $this->url['query']);
		}
		
		return $requesturi;
	}
}
//
?>